﻿using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Phoenix.Xna.Framework.FrameworkExtensions;
using Phoenix.Xna.Framework.Graphics;

namespace Phoenix.Xna.Framework.Components.Terrain
{
    public class ProgressiveTerrain : DrawableComponent, IImplementsEffectMap
    {
        internal const int MaxProcessIterations = 256;
        internal const byte MaxDepth = 8;
        private string _heightMapAsset;
        private int? _smoothingPasses;
        private HeightMap _heightMap;
        private string _effectMapAsset;
        private List<TerrainDataBuffer> _buffersToDispose = new List<TerrainDataBuffer>();
        private List<TerrainDataBuffer> _loadedBuffers = new List<TerrainDataBuffer>();
        private bool _update;
        private float _verticeMinDistance;
        private VertexDeclaration _vertexDeclaration;
        private TerrainDataBuffer _currentBufferData;
        private Vector3 _eulerRotation;
        private Thread _updateThread;
        private AutoResetEvent _updateCompleteEvent;

        public TerrainNode Root { get; set; }
        public List<VertexPositionNormal> Vertices { get; private set; }
        public List<int> Indices { get; private set; }
        public IShader<TerrainDataBuffer> Shader { get; set; }
        public float NodeRelevance { get; set; }
        public float VertexDetail { get; set; }
        public float NearDetailThreshold { get; set; }
        public float FarDetailThreshold { get; set; }
        public int Width { get { return (int)(_heightMap.Width * Scale.X); } }
        public int Height { get { return (int)(_heightMap.Height * Scale.X); } }
        internal int MinNodeDepth { get; private set; }
        internal int ProcessIterationCount { get; private set; }

        public ProgressiveTerrain(string heightMap, int? smoothingPasses, Vector2 scale, string effectMap)
            : base()
        {
            Initialize(heightMap, smoothingPasses, scale, effectMap);
        }

        public ProgressiveTerrain(string id, string heightMap, int? smoothingPasses, Vector2 scale, string effectMap)
            : base(id)
        {
            Initialize(heightMap, smoothingPasses, scale, effectMap);
        }

        public float GetHeight(float xPos, float zPos)
        {
            int left = (int)Math.Abs((xPos / (int)Scale.X) % _heightMap.Width);
            int top = (int)Math.Abs((zPos / (int)Scale.X) % _heightMap.Height);
            float cZ;

            if (top > 0 && left > 0 && top < _heightMap.Height - 1 && left < _heightMap.Width - 1)
            {
                float rightDiagDifference = _heightMap[left, top] - _heightMap[left + 1, top + 1];
                float leftDiagDifference = _heightMap[left + 1, top] - _heightMap[left, top + 1];

                if (Math.Abs(rightDiagDifference) >= Math.Abs(leftDiagDifference))
                {
                    if (-((xPos % Scale.X) / Scale.X) + 1 > (zPos % Scale.X) / Scale.X)
                    {
                        float xNormalized = (xPos % Scale.X) / Scale.X;
                        float zNormalized = (zPos % Scale.X) / Scale.X;
                        float topHeight = MathHelper.Lerp(
                          _heightMap[left, top],
                          _heightMap[left + 1, top],
                          xNormalized);
                        float bottomHeight = MathHelper.Lerp(
                          _heightMap[left, top + 1],
                          _heightMap[left, top + 1] - (_heightMap[left, top] - _heightMap[left + 1, top]),
                          xNormalized);
                        cZ = MathHelper.Lerp(topHeight, bottomHeight, zNormalized);
                    }
                    else
                    {
                        float xNormalized = (xPos % Scale.X) / Scale.X;
                        float zNormalized = (zPos % Scale.X) / Scale.X;
                        float topHeight = MathHelper.Lerp(
                          _heightMap[left, top + 1],
                          _heightMap[left + 1, top + 1],
                          xNormalized);
                        float bottomHeight = MathHelper.Lerp(
                          _heightMap[left + 1, top] + (_heightMap[left, top + 1] - _heightMap[left + 1, top + 1]),
                          _heightMap[left + 1, top],
                          xNormalized);
                        cZ = MathHelper.Lerp(bottomHeight, topHeight, zNormalized);
                    }
                }
                else
                {
                    if (((xPos % Scale.X) / Scale.X) > (zPos % Scale.X) / Scale.X)
                    {
                        float xNormalized = (xPos % Scale.X) / Scale.X;
                        float zNormalized = (zPos % Scale.X) / Scale.X;
                        float topHeight = MathHelper.Lerp(
                          _heightMap[left, top],
                          _heightMap[left + 1, top],
                          xNormalized);
                        float bottomHeight = MathHelper.Lerp(
                          _heightMap[left + 1, top + 1] + (_heightMap[left, top] - _heightMap[left + 1, top]),
                          _heightMap[left + 1, top + 1],
                          xNormalized);
                        cZ = MathHelper.Lerp(topHeight, bottomHeight, zNormalized);
                    }
                    else
                    {
                        float xNormalized = (xPos % Scale.X) / Scale.X;
                        float zNormalized = (zPos % Scale.X) / Scale.X;
                        float topHeight = MathHelper.Lerp(
                          _heightMap[left, top + 1],
                          _heightMap[left + 1, top + 1],
                          xNormalized);
                        float bottomHeight = MathHelper.Lerp(
                          _heightMap[left, top],
                          _heightMap[left, top] - (_heightMap[left, top + 1] - _heightMap[left + 1, top + 1]),
                          xNormalized);
                        cZ = MathHelper.Lerp(bottomHeight, topHeight, zNormalized);
                    }
                }
            }
            else
            {
                cZ = _heightMap[left, top];
            }

            cZ *= Scale.Y;
            return cZ;
        }

        public float GetNodeSizeAtLevel(int depth)
        {
            int diff = (int)((MaxDepth - 1) - depth);
            double result = System.Math.Pow(2, diff);
            return _verticeMinDistance * (float)result;
        }

        private void Initialize(string heightMapAsset, int? smoothingPasses, Vector2 scale, string effectMap)
        {
            _heightMapAsset = heightMapAsset;
            _smoothingPasses = smoothingPasses;
            Vertices = new List<VertexPositionNormal>(1000);
            Indices = new List<int>(1000);
            NearDetailThreshold = 2500f;
            FarDetailThreshold = 100000f;
            VertexDetail = 17f;
            NodeRelevance = 0.1f;
            Scale = scale;
            _effectMapAsset = effectMap;
        }

        private void UpdateCallback()
        {
            while (_update)
            {
                Thread.Sleep(200);
                ProcessIterationCount += 1;

                if (ProcessIterationCount > MaxProcessIterations)
                {
                    ProcessIterationCount = 0;
                }

                Root.Update();
                InitializeBuffers();
                DisposeLastBuffer();
                _updateCompleteEvent.Set();
            }
        }

        private void DisposeLastBuffer()
        {
            if (_buffersToDispose != null)
            {
                while (_buffersToDispose.Count > 0)
                {
                    TerrainDataBuffer data = _buffersToDispose[0];
                    _buffersToDispose.RemoveAt(0);
                    data.VertexBuffer.Dispose();
                    data.VertexBuffer = null;
                    data.IndexBuffer.Dispose();
                    data.IndexBuffer = null;
                }
            }
        }

        private void InitializeBuffers()
        {
            Vertices.Clear();
            Indices.Clear();
            Root.BuildNode();

            if (Engine.Instance.GraphicsDevice != null && !Engine.Instance.GraphicsDevice.IsDisposed)
            {
                IndexBuffer indexBuffer;
                VertexBuffer vertexBuffer;
                vertexBuffer = new VertexBuffer(Engine.Instance.GraphicsDevice, typeof(VertexPositionNormal), Vertices.Count, BufferUsage.WriteOnly);
                vertexBuffer.SetData<VertexPositionNormal>(Vertices.ToArray());
                indexBuffer = new IndexBuffer(Engine.Instance.GraphicsDevice, typeof(int), Indices.Count, BufferUsage.WriteOnly);
                indexBuffer.SetData<int>(Indices.ToArray());
                TerrainDataBuffer buffer = new TerrainDataBuffer();
                buffer.IndexBuffer = indexBuffer;
                buffer.VertexBuffer = vertexBuffer;
                buffer.NumberOfIndices = Indices.Count / 3;
                buffer.NumberOfVertices = Vertices.Count;
                _loadedBuffers.Add(buffer);
            }
        }

        #region IComponent Members

        public override void Initialize()
        {
            if (!IsInitialized)
            {
                _heightMap = new HeightMap(Engine.Instance.Content, _heightMapAsset, _smoothingPasses);
                _verticeMinDistance = (float)((_heightMap.Width * Scale.X) / System.Math.Pow(2, MaxDepth - 1));
                Root = new TerrainNode(null, NodeChild.NorthEast);
                Root.Position = Position.ToVector2();
                Root.Tree = this;
                Root.Initialize();
                base.Initialize();
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (!IsDisposed && disposing)
            {
                UnloadContent();
                IsDisposed = true;
            }
        }

        #endregion

        #region IDrawableComponent Members

        public override void LoadContent()
        {
            if (!IsContentLoaded)
            {
                if (Shader == null)
                {
                    Shader = new TerrainShader(_effectMapAsset, true) { BlendMapScaler = _heightMap.Width * Scale.X };
                }

                EffectMap = Shader.EffectMap;
                MinNodeDepth = 4;
                Root.Update();
                MinNodeDepth = 1;
                InitializeBuffers();
                _currentBufferData = _loadedBuffers[0];
                _vertexDeclaration = new VertexDeclaration(Engine.Instance.GraphicsDevice, VertexPositionNormal.VertexElements);
                Engine.Instance.GraphicsDevice.VertexDeclaration = _vertexDeclaration;
                Engine.Instance.GraphicsDevice.Vertices[0].SetSource(_currentBufferData.VertexBuffer, 0, VertexPositionNormal.SizeInBytes);
                Engine.Instance.GraphicsDevice.Indices = _currentBufferData.IndexBuffer;
                _update = true;

                // TODO : Fix this, need to be able to kill the thread, don't use the threadpool.
                //ThreadPool.QueueUserWorkItem(UpdateCallback);
                _updateCompleteEvent = new AutoResetEvent(false);
                _updateThread = new Thread(new ThreadStart(UpdateCallback));
                _updateThread.Start();
                base.LoadContent();
            }
        }

        public override void UnloadContent()
        {
            if (IsContentLoaded)
            {
                _update = false;
                _updateCompleteEvent.WaitOne();
                Root.Dispose();

                for (int i = 0; i < this._buffersToDispose.Count; i++)
                {
                    TerrainDataBuffer data = _buffersToDispose[i];
                    data.VertexBuffer.Dispose();
                    data.VertexBuffer = null;
                    data.IndexBuffer.Dispose();
                    data.IndexBuffer = null;
                }

                _buffersToDispose.Clear();
                _buffersToDispose = null;

                for (int i = 0; i < this._loadedBuffers.Count; i++)
                {
                    TerrainDataBuffer data = _loadedBuffers[i];
                    data.VertexBuffer.Dispose();
                    data.VertexBuffer = null;
                    data.IndexBuffer.Dispose();
                    data.IndexBuffer = null;
                    _loadedBuffers.RemoveAt(0);
                }

                _loadedBuffers.Clear();
                _loadedBuffers = null;
                Shader.Dispose();
                base.UnloadContent();
            }
        }

        public override void Draw(GameTime gameTime)
        {
            if (Visible)
            {
                if (_loadedBuffers.Count > 0)
                {
                    this._buffersToDispose.Add(this._currentBufferData);
                    this._currentBufferData = _loadedBuffers[0];
                }

                Engine.Instance.GraphicsDevice.VertexDeclaration = _vertexDeclaration;
                Shader.Draw(gameTime, _currentBufferData);

                if (_loadedBuffers.Count > 0)
                {
                    _loadedBuffers.RemoveAt(0);
                }
            }
        }

        #endregion

        #region IImplementsEffectMap Members

        public EffectMap EffectMap
        {
            get { return Shader.EffectMap; }
            set { Shader.EffectMap = value; }
        }

        public void LoadEffectMap(string effectMap)
        {
            if (EffectMap != null)
            {
                EffectMap.Dispose();
            }

            EffectMap = Engine.Instance.Content.Load<EffectMap>(effectMap);
        }

        #endregion

        #region I3DComponent Members

        public Vector3 Position { get; set; }

        public Vector2 Scale { get; set; }

        public Vector3 EulerRotation
        {
            get { return _eulerRotation; }
            set
            {
                _eulerRotation = value;
                Rotation = Matrix.CreateFromYawPitchRoll(_eulerRotation.X, _eulerRotation.Y, _eulerRotation.Z);
            }
        }

        public Matrix Rotation { get; protected set; }

        public BoundingBox BoundingBox { get; protected set; }

        public void SetPosition(float? x, float? y, float? z)
        {
            float xPos = (x == null) ? Position.X : x.Value;
            float yPos = (y == null) ? Position.Y : y.Value;
            float zPos = (x == null) ? Position.Z : z.Value;
            Position = new Vector3(xPos, yPos, zPos);
        }

        #endregion
    }
}
